#############################################################################
# Copyright (c) 2009 Dmitry Grushin <dgrushin@gmail.com>.
#
# This file is part of clupower (cluster power manager utility)
#
# Clupower is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Clupower is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Clupower.  If not, see <http://www.gnu.org/licenses/>.
#
# Contributors:
#    Dmitry Grushin <dgrushin@gmail.com> - initial API and implementation
#############################################################################

require 'socket'
require 'settings'
require 'commands'
require 'cpmonitor'
require 'algo'
require 'set'
require 'errors'
require 'thread'
require 'cputil'
require 'rubygems'
require 'daemons/daemonize'

class CPServer
  include Daemonize

  def initialize
    # The list of node ids that we switched off
    @hold_nodes = []
    # The list of JobsWaitNodes objects which wait for nodes to
    # clear hold status from jobs
    @node_wait_list = []
    @start_date = Time.now
    @error_count = 0
  end

  # Issues power on command for the list of nodes
  def nodes_on(node_list)
    for node in node_list do
      begin
        Cmd.instance.node_on node
        @hold_nodes.delete(node)
      rescue CPRuntimeError => e
        CPMonitor.instance.log_error(e.get_trace)
      end
    end
  end

  # Issues power off command for the list of nodes
  def nodes_sleep(node_list)
    for node in node_list do
      begin
        @hold_nodes << node
        Cmd.instance.node_off node
      rescue CPRuntimeError => e
        CPMonitor.instance.log_error(e.get_trace)
      end
    end
  end

  # Power management iteration
  def manage_power
    @node_wait_list.delete_if { |wl| wl.done }
    if Settings.instance.pm_algo.class != @last_algo_class
      CPMonitor.instance.
        log_message("Power manager heuristics changed: #{Settings.instance.pm_algo.class}")
      @last_algo_class = Settings.instance.pm_algo.class
    end
    pm_decision = Settings.instance.pm_algo.manage(@hold_nodes, @node_wait_list)
    nodes_sleep pm_decision.nodes_sleep
    nodes_on pm_decision.nodes_on
    unless pm_decision.nodes_on.empty?
      @node_wait_list << JobsWaitNodes.new(pm_decision.jobs_unlock, pm_decision.nodes_on).wait
    else
      pm_decision.jobs_unlock.each do |job|
        begin
          Cmd.instance.release_job_hold(job) if Cmd.instance.job_deferred?(job)
        rescue CPRuntimeError => e
          CPMonitor.instance.log_error("Error occured while trying to release job hold #{job}: #{e.message}.")
        end
      end
    end
    # clear possible defered status from some jobs
  rescue CPRuntimeError => e
    CPMonitor.instance.log_error(e.get_trace)
    @error_count += 1
  end

  # Closes power manager:
  # 1) remove all holds
  # 2) turn on all sleeping nodes
  def shutdown(leave_nodes)
    CPMonitor.instance.log_message("Stop command received.")

    # Flush stats buffer
    CPMonitor.instance.finish

    unless @hold_nodes.empty?
      if leave_nodes
        CPMonitor.instance.log_message("Leaving #{@hold_nodes.size} nodes off.")
      else
        CPMonitor.instance.log_message("Switching on #{@hold_nodes.size} nodes" +
            " that were powered off by the manager.")
        @hold_nodes.each { |node| Cmd.instance.node_on(node) }
      end
    end
        
    if !@node_wait_list.empty? && !leave_nodes
      CPMonitor.instance.log_message("Wait for nodes to release job holds...")
      @node_wait_list.each { |wl| wl.join }
    end
    
    CPMonitor.instance.log_message("Power manager finished.")
  end

  def run(as_daemon=false)
    if File.exist?(Utils.get_lock_file)
      raise CPI3.new(Utils.get_lock_file)
    end

    if as_daemon
      CPMonitor.rotate_log(Settings.instance.get_main_daemon_log_path)
      daemonize(Settings.instance.get_main_daemon_log_path, "#{$0} #{$*.join(' ')}")
    end
    main_loop
  end

  private

  def main_loop
    leave_nodes = false
    stop = false
    finished = false
    key = rand

    # Write a key to the file. A user command will use this key.
    open(Utils.get_lock_file, "w") { |f| f.puts key}
    File.chmod(0600, Utils.get_lock_file)

    control = Thread.new do
      begin
        dts = TCPServer.new('127.0.0.1', Settings.instance.get('SERVER_PORT'))
        until stop
          s = dts.accept
          cmd = s.recv( 100 )
          case
          when cmd == "STOP#{key}"
            stop = true
          when cmd == "EXIT#{key}"
            stop = true
            leave_nodes = true
          when cmd == "RELOAD#{key}"
            begin
              Settings.instance.load
              response = "Done."
            rescue CPInitError => e
              response = "Failed. Error: #{e.get_trace}"
            end
            s.write(response)
            s.close
          when cmd == "STATUS#{key}"
            s.write("running since #{@start_date}, #{@error_count} errors")
            s.close
          when cmd =~ /NODESTATUS#{key}(.*)/
            if @hold_nodes.include?($1)
              s.write("Sleeping")
            else
              s.write("Unknown")
            end
            s.close
          else
          end
        end

        until finished
          sleep(1)
        end

        s.write("Finished.")
        s.close

      rescue Exception => err
        CPMonitor.instance.log_error("Error in server socket loop: #{err.message}")
        CPMonitor.instance.log_error("Trace: #{err.backtrace.join("; ")}")
        stop = true
      end
    end
    
    ['TERM', 'INT'].each do |signal|
      trap(signal) do
        CPClient.new.send_shutdown(true).close
        puts "Stoping server ..."
      end
    end

    CPMonitor.instance.log_message("Power manager started.")

    count = 0
    until stop
      if count >= Settings.instance.get('SLEEP_PAUSE')
        count = 0
        Cmd.instance.update_nodes_idle_timers
        manage_power
        CPMonitor.instance.print_stats
      end
      count += sleep 2
    end
    shutdown(leave_nodes)
  ensure
    finished = true
    raise CPE22.new if control != nil && control.join(600) == nil
    File.delete(Utils.get_lock_file)
    puts "Server stopped."
  end
end
